<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go OTel 应用逻辑解释 - 业务逻辑服务</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
<h1>业务逻辑服务 (Services)</h1>

<nav>
    <ul>
        <li><a href="index.html">概述</a></li>
        <li><a href="setup.html">初始化设置</a></li>
        <li><a href="handlers.html">HTTP 处理器</a></li>
        <li><a href="services.html" class="active">业务逻辑服务</a></li>
        <li><a href="main_flow.html">主程序流程</a></li>
        <li><a href="glossary.html">名词解释</a></li>
    </ul>
</nav>

<section>
    <h2>引言</h2>
    <p>
        业务逻辑层（或称为服务层）封装了应用程序的核心功能。它不直接处理 HTTP 请求，而是被 HTTP 处理器调用。这一层负责与数据存储（数据库、缓存）交互，执行业务规则，并返回结果或错误给调用者（处理器）。
    </p>
    <p>
        在这一层继续传递 <a href="glossary.html#context"><code>context.Context</code></a> 非常重要，因为它携带了上层（Handler）创建的 <a href="glossary.html#span">Span</a> 信息。这样，当业务逻辑层内部创建新的 Span，或者调用被 OTel 自动插桩的库（如 <a href="glossary.html#otelgorm"><code>otelgorm</code></a> 和 <a href="glossary.html#redisotel"><code>redisotel</code></a>）时，新创建的 Span 都能正确地成为调用链的一部分。
    </p>
</section>

<section id="find-user-service">
    <h2>1. 查找用户服务 (<code>findUserByID</code>)</h2>
    <p>
        此函数负责根据用户 ID 查找用户，并实现了“缓存优先”策略。
    </p>
    <pre><code class="language-go">// findUserByID 业务逻辑：根据 ID 查找用户（缓存优先）。
func findUserByID(ctx context.Context, id string) (*User, error) {
    <span class="code-comment">// 1. 创建业务逻辑 Span (作为 Handler Span 的子 Span)</span>
    var bizSpan trace.Span
    ctx, bizSpan = tracer.Start(ctx, "Service: FindUserByID",
        trace.WithAttributes(attribute.String("app.user.id", id)),
        trace.WithSpanKind(trace.SpanKindInternal),
    )
    defer bizSpan.End()

    var user User
    var cacheHit bool = false

    <span class="code-comment">// --- 2. 查询 Redis 缓存 ---</span>
    cacheKey := fmt.Sprintf("user:%s", id)
    bizSpan.AddEvent("检查 Redis 缓存", trace.WithAttributes(/*...*/))
    <span class="code-comment">// redisotel 插件会自动为此 Get 操作创建 Span</span>
    jsonData, err := rdb.Get(ctx, cacheKey).Result()

    <span class="code-comment">// --- 3. 处理缓存结果 ---</span>
    if err == nil { <span class="code-comment">// 缓存命中</span>
        cacheHit = true
        bizSpan.AddEvent("缓存命中")
        bizSpan.SetAttributes(attribute.Bool("cache.hit", true))
        if errUnmarshal := json.Unmarshal([]byte(jsonData), &user); errUnmarshal != nil {
             <span class="code-comment">// 反序列化失败，记录错误，继续查库</span>
            bizSpan.RecordError(errUnmarshal, trace.WithAttributes(/*...*/))
            bizSpan.SetStatus(codes.Error, "缓存数据反序列化失败")
            cacheHit = false <span class="code-comment">// 标记为未命中，以便后续查库</span>
        } else {
             <span class="code-comment">// 成功从缓存获取</span>
            bizSpan.SetStatus(codes.Ok, "用户在缓存中找到")
            return &user, nil <span class="code-comment">// 直接返回</span>
        }
    } else if !errors.Is(err, redis.Nil) { <span class="code-comment">// Redis 其他错误</span>
        bizSpan.RecordError(err, trace.WithAttributes(/*...*/))
        bizSpan.SetStatus(codes.Error, "Redis 缓存检查失败")
        <span class="code-comment">// 继续查库</span>
    } else { <span class="code-comment">// 缓存未命中 (err == redis.Nil)</span>
        bizSpan.AddEvent("缓存未命中")
        bizSpan.SetAttributes(attribute.Bool("cache.hit", false))
    }

    <span class="code-comment">// --- 4. 查询 MySQL (仅当缓存未命中或无效时) ---</span>
    if !cacheHit {
        bizSpan.AddEvent("查询数据库")
        <span class="code-comment">// otelgorm 插件会自动为此 First 操作创建 Span</span>
        result := db.WithContext(ctx).First(&user, "id = ?", id)

        <span class="code-comment">// --- 5. 处理数据库结果 ---</span>
        if result.Error != nil {
            bizSpan.RecordError(result.Error)
            <span class="code-comment">// ... 设置 Span 状态 (Error) ...</span>
            return nil, result.Error <span class="code-comment">// 返回数据库错误</span>
        }
        bizSpan.AddEvent("数据库查询成功")

        <span class="code-comment">// --- 6. 写回 Redis 缓存 ---</span>
        bizSpan.AddEvent("更新 Redis 缓存")
        jsonDataBytes, errMarshal := json.Marshal(&user)
        if errMarshal != nil {
            <span class="code-comment">// ... 记录错误，但不阻塞 ...</span>
            bizSpan.SetStatus(codes.Ok, "用户在数据库中找到，但缓存更新失败 (序列化错误)")
            return &user, nil <span class="code-comment">// 仍返回从数据库获取的数据</span>
        }
        cacheTTL := time.Duration(5+rand.Intn(6)) * time.Minute <span class="code-comment">// 随机 TTL</span>
        <span class="code-comment">// redisotel 插件会自动为此 Set 操作创建 Span</span>
        setCmd := rdb.Set(ctx, cacheKey, string(jsonDataBytes), cacheTTL)
        if errSet := setCmd.Err(); errSet != nil {
            <span class="code-comment">// ... 记录错误，但不阻塞 ...</span>
             bizSpan.SetStatus(codes.Ok, "用户在数据库中找到，但缓存更新失败 (Redis Set 错误)")
            return &user, nil <span class="code-comment">// 仍返回从数据库获取的数据</span>
        }
        bizSpan.AddEvent("缓存更新成功")
        bizSpan.SetAttributes(attribute.Bool("cache.updated", true))
        bizSpan.SetStatus(codes.Ok, "用户在数据库中找到并已更新缓存")
        return &user, nil <span class="code-comment">// 返回从数据库获取的数据</span>
    }

    <span class="code-comment">// 正常流程不应到达这里</span>
    bizSpan.SetStatus(codes.Error, "内部逻辑错误")
    return nil, errors.New("findUserByID 内部逻辑错误")
}
</code></pre>
    <h3>缓存优先逻辑流程图:</h3>
    <div class="diagram">
        <svg width="600" height="850" xmlns="http://www.w3.org/2000/svg">
            <defs><marker id="arrowhead" markerWidth="10" markerHeight="7" refX="0" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#6c757d" /></marker></defs>

            <rect x="200" y="20" width="200" height="40" class="svg-rect"/>
            <text x="300" y="40" class="svg-text">进入 findUserByID</text>

            <rect x="200" y="90" width="200" height="40" class="svg-rect"/>
            <text x="300" y="110" class="svg-text">创建 Service Span</text>

            <rect x="200" y="160" width="200" height="50" class="svg-rect"/>
            <text x="300" y="180" class="svg-text">检查 Redis 缓存 (Get)</text>
            <text x="300" y="195" class="svg-text" style="font-size:10px;">(redisotel 自动创建 Span)</text>

            <polygon points="300,230 400,270 300,310 200,270" class="svg-diamond"/>
            <text x="300" y="265" class="svg-text">Redis Get</text>
            <text x="300" y="280" class="svg-text">出错 (非 Nil)?</text>

            <polygon points="500,250 560,290 500,330 440,290" class="svg-diamond" transform="translate(30, 0)"/>
            <text x="530" y="290" class="svg-text">缓存命中?</text>
            <text x="530" y="305" class="svg-text" style="font-size:10px;">(err == nil)</text>

            <rect x="470" y="360" width="120" height="40" class="svg-rect" transform="translate(30, 0)"/>
            <text x="560" y="380" class="svg-text">反序列化 JSON</text>

            <polygon points="500,420 560,460 500,500 440,460" class="svg-diamond" transform="translate(30, 0)"/>
            <text x="530" y="460" class="svg-text">反序列化成功?</text>

            <rect x="470" y="530" width="120" height="40" class="svg-rect" transform="translate(30, 0)"/>
            <text x="560" y="550" class="svg-text">记录反序列化错误</text>

            <rect x="200" y="340" width="200" height="50" class="svg-rect"/>
            <text x="300" y="360" class="svg-text">查询 MySQL (First)</text>
            <text x="300" y="375" class="svg-text" style="font-size:10px;">(otelgorm 自动创建 Span)</text>

            <polygon points="300,410 400,450 300,490 200,450" class="svg-diamond"/>
            <text x="300" y="445" class="svg-text">数据库查询</text>
            <text x="300" y="460" class="svg-text">成功?</text>

            <rect x="200" y="520" width="200" height="40" class="svg-rect"/>
            <text x="300" y="540" class="svg-text">序列化用户数据 (JSON)</text>

            <polygon points="300,580 400,620 300,660 200,620" class="svg-diamond"/>
            <text x="300" y="615" class="svg-text">序列化</text>
            <text x="300" y="630" class="svg-text">成功?</text>

            <rect x="200" y="690" width="200" height="50" class="svg-rect"/>
            <text x="300" y="710" class="svg-text">写入 Redis 缓存 (Set)</text>
            <text x="300" y="725" class="svg-text" style="font-size:10px;">(redisotel 自动创建 Span)</text>

            <polygon points="300,760 400,800 300,840 200,800" class="svg-diamond"/>
            <text x="300" y="795" class="svg-text">Redis Set</text>
            <text x="300" y="810" class="svg-text">成功?</text>

            <rect x="450" y="50" width="140" height="40" class="svg-rect"/>
            <text x="520" y="70" class="svg-text">记录 Redis Get 错误</text>

            <rect x="50" y="430" width="140" height="40" class="svg-rect"/>
            <text x="120" y="450" class="svg-text">记录 DB 查询错误</text>

            <rect x="450" y="600" width="140" height="40" class="svg-rect"/>
            <text x="520" y="620" class="svg-text">记录序列化错误</text>

            <rect x="450" y="780" width="140" height="40" class="svg-rect"/>
            <text x="520" y="800" class="svg-text">记录 Redis Set 错误</text>

            <rect x="630" y="440" width="120" height="40" class="svg-rect"/>
            <text x="690" y="460" class="svg-text">返回用户数据</text>

            <rect x="50" y="780" width="140" height="40" class="svg-rect"/>
            <text x="120" y="800" class="svg-text">返回最终结果</text>

            <!-- Arrows -->
            <line x1="300" y1="60" x2="300" y2="90" class="svg-arrow"/>
            <line x1="300" y1="130" x2="300" y2="160" class="svg-arrow"/>
            <line x1="300" y1="210" x2="300" y2="230" class="svg-arrow"/>

            <line x1="400" y1="270" x2="485" y2="70" class="svg-arrow"/> <text x="440" y="160" class="svg-label">是 (错误)</text>
            <line x1="520" y1="90" x2="300" y2="340" class="svg-arrow"/> <text x="410" y="190" class="svg-label">记录后继续查库</text>

            <line x1="300" y1="310" x2="470" y2="290" class="svg-arrow"/> <text x="380" y="295" class="svg-label">否 (无错误)</text>
            <line x1="530" y1="330" x2="530" y2="360" class="svg-arrow"/> <text x="555" y="345" class="svg-label">是 (命中)</text>
            <line x1="530" y1="400" x2="530" y2="420" class="svg-arrow"/>

            <line x1="560" y1="460" x2="630" y2="460" class="svg-arrow"/> <text x="595" y="450" class="svg-label">是</text>
            <line x1="440" y1="460" x2="500" y2="530" class="svg-arrow"/> <text x="445" y="495" class="svg-label">否</text>
            <line x1="560" y1="570" x2="300" y2="340" class="svg-arrow"/> <text x="450" y="450" class="svg-label">记录后继续查库</text>


            <line x1="470" y1="290" x2="300" y2="340" class="svg-arrow"/> <text x="380" y="320" class="svg-label">否 (未命中)</text>
            <line x1="300" y1="390" x2="300" y2="410" class="svg-arrow"/>

            <line x1="200" y1="450" x2="120" y2="450" class="svg-arrow"/> <text x="160" y="440" class="svg-label">否 (错误)</text>
            <line x1="120" y1="470" x2="120" y2="780" class="svg-arrow"/> <text x="100" y="600" class="svg-label">返回错误</text>


            <line x1="300" y1="490" x2="300" y2="520" class="svg-arrow"/> <text x="325" y="505" class="svg-label">是 (成功)</text>
            <line x1="300" y1="560" x2="300" y2="580" class="svg-arrow"/>

            <line x1="400" y1="620" x2="520" y2="620" class="svg-arrow"/> <text x="460" y="610" class="svg-label">否</text>
            <line x1="520" y1="640" x2="120" y2="780" class="svg-arrow"/> <text x="320" y="700" class="svg-label">记录后返回成功</text>


            <line x1="300" y1="660" x2="300" y2="690" class="svg-arrow"/> <text x="325" y="675" class="svg-label">是</text>
            <line x1="300" y1="740" x2="300" y2="760" class="svg-arrow"/>

            <line x1="400" y1="800" x2="520" y2="800" class="svg-arrow"/> <text x="460" y="790" class="svg-label">否</text>
            <line x1="520" y1="820" x2="120" y2="780" class="svg-arrow"/> <text x="320" y="830" class="svg-label">记录后返回成功</text>


            <line x1="200" y1="800" x2="120" y2="800" class="svg-arrow"/> <text x="160" y="790" class="svg-label">是</text>


        </svg>
    </div>
    <h3>解释:</h3>
    <ol>
        <li><strong>创建 Service Span:</strong> 函数开始时，创建一个名为 "Service: FindUserByID" 的内部 Span，它是调用者（Handler）Span 的子 Span。</li>
        <li><strong>查缓存 (Redis Get):</strong> 构造缓存 Key，然后调用 <code>rdb.Get</code>。<code>redisotel</code> 插件会自动记录这次 Redis 操作。</li>
        <li><strong>缓存命中处理:</strong>
            <ul>
                <li>如果 <code>rdb.Get</code> 返回 <code>nil</code> 错误，表示缓存命中。</li>
                <li>尝试用 <code>json.Unmarshal</code> 将缓存数据解析到 <code>user</code> 结构体。</li>
                <li>如果反序列化成功，设置 Span 状态为 OK，直接返回用户数据，流程结束。</li>
                <li>如果反序列化失败，记录错误，将 Span 状态标记为错误，<span class="highlight">但流程继续</span>，将 <code>cacheHit</code> 设为 <code>false</code>，准备查询数据库（将无效缓存视为未命中）。</li>
            </ul>
        </li>
        <li><strong>Redis 错误处理:</strong> 如果 <code>rdb.Get</code> 返回的错误不是 <code>redis.Nil</code>（表示不是 Key 不存在，而是其他 Redis 连接或操作错误），记录错误，设置 Span 状态为错误，<span class="highlight">但流程继续</span>，准备查询数据库（因为缓存此时不可靠）。</li>
        <li><strong>缓存未命中处理:</strong> 如果 <code>rdb.Get</code> 返回 <code>redis.Nil</code>，表示缓存未命中。添加 "Cache Miss" 事件，设置 <code>cache.hit=false</code> 属性，流程继续。</li>
        <li><strong>查数据库 (GORM First):</strong> 仅当 <code>cacheHit</code> 为 <code>false</code> 时执行。调用 <code>db.WithContext(ctx).First(...)</code>。<code>otelgorm</code> 插件会自动记录这次数据库查询。</li>
        <li><strong>数据库结果处理:</strong>
            <ul>
                <li>如果 GORM 返回错误（包括 <code>gorm.ErrRecordNotFound</code>），记录错误，设置 Span 状态为错误，并将数据库错误返回给上层 Handler。</li>
                <li>如果查询成功，添加成功事件。</li>
            </ul>
        </li>
        <li><strong>写回缓存 (Redis Set):</strong> 如果数据库查询成功，尝试将查到的用户数据序列化为 JSON，并使用 <code>rdb.Set</code> 将其写入 Redis。<code>redisotel</code> 会自动追踪这次 Set 操作。这里还使用了随机的 TTL (Time-To-Live) 来避免缓存同时失效（缓存雪崩）。</li>
        <li><strong>缓存写入错误处理:</strong> 如果序列化或 Redis Set 操作失败，记录相应的错误到 Span，并添加警告事件。<span class="highlight">重要的是，即使缓存写回失败，我们仍然认为主要操作（从数据库获取用户）是成功的</span>，所以将 Span 状态设为 OK，并返回从数据库获取的用户数据。</li>
        <li><strong>最终成功:</strong> 如果数据库查询成功并且缓存写回也成功（或虽失败但非阻塞），设置 Span 状态为 OK，返回用户数据。</li>
    </ol>
    <p>这个函数的容错性设计很重要：缓存的读取或写入失败不应阻止从数据库获取数据的核心流程。</p>
</section>

<section id="save-user-service">
    <h2>2. 保存用户服务 (<code>saveUser</code>)</h2>
    <p>
        此函数负责将新用户信息保存到数据库，并尝试使相关缓存失效。
    </p>
    <pre><code class="language-go">// saveUser 业务逻辑：保存新用户到数据库，并使相关缓存失效。
func saveUser(ctx context.Context, user *User) (*User, error) {
    // 1. 创建业务逻辑 Span
    var bizSpan trace.Span
    ctx, bizSpan = tracer.Start(ctx, "Service: SaveUser", /*...*/ )
    defer bizSpan.End()

    var dbErrorOccurred bool = false
    var cacheErrorOccurred bool = false
    var dbErr, cacheErr error

    // --- 2. 保存用户到数据库 ---
    bizSpan.AddEvent("开始保存用户到数据库")
    // otelgorm 自动追踪 Create 操作
    result := db.WithContext(ctx).Create(user)
    if result.Error != nil {
        dbErrorOccurred = true
        dbErr = result.Error
        bizSpan.RecordError(dbErr, trace.WithAttributes(/*...*/))
        bizSpan.SetStatus(codes.Error, "保存用户到数据库失败")
        return nil, fmt.Errorf("数据库创建用户失败: %w", dbErr) // 返回错误
    }
    bizSpan.AddEvent("用户成功保存到数据库")

    // --- 3. 使 Redis 缓存失效 ---
    bizSpan.AddEvent("使 Redis 缓存失效")
    cacheKey := fmt.Sprintf("user:%s", user.ID)
    // redisotel 自动追踪 Del 操作
    delCmd := rdb.Del(ctx, cacheKey)
    deletedCount := delCmd.Val()
    if err := delCmd.Err(); err != nil && !errors.Is(err, redis.Nil) { // 忽略 key 不存在错误
        cacheErrorOccurred = true
        cacheErr = err
        bizSpan.RecordError(cacheErr, trace.WithAttributes(/*...*/))
        bizSpan.AddEvent("警告: Redis 缓存失效失败")
        log.Printf("警告: 失效用户 '%s' 的 Redis 缓存失败: %v", user.ID, cacheErr)
        // 缓存失效失败通常不阻塞主流程
    } else {
        // ... 添加缓存失效成功或无需失效的事件 ...
    }

    // --- 4. 设置最终 Span 状态 ---
    if dbErrorOccurred { /* 错误已设置 */ } else if cacheErrorOccurred {
        // DB OK, Cache Error -> Status OK, message indicates warning
        bizSpan.SetStatus(codes.Ok, "用户已保存到数据库，但缓存失效失败")
    } else {
        // DB OK, Cache OK -> Status OK
        bizSpan.SetStatus(codes.Ok, "用户成功保存，缓存已处理")
    }

    // 返回创建的用户对象和 nil 错误 (即使缓存失效失败)
    return user, nil
}</code></pre>
    <h3>逻辑流程图:</h3>
    <div class="diagram">
        <svg width="400" height="650" xmlns="http://www.w3.org/2000/svg">
            <defs><marker id="arrowhead" markerWidth="10" markerHeight="7" refX="0" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#6c757d" /></marker></defs>

            <rect x="100" y="20" width="200" height="40" class="svg-rect"/>
            <text x="200" y="40" class="svg-text">进入 saveUser</text>

            <rect x="100" y="90" width="200" height="40" class="svg-rect"/>
            <text x="200" y="110" class="svg-text">创建 Service Span</text>

            <rect x="100" y="160" width="200" height="50" class="svg-rect"/>
            <text x="200" y="180" class="svg-text">保存到 MySQL (Create)</text>
            <text x="200" y="195" class="svg-text" style="font-size:10px;">(otelgorm 自动创建 Span)</text>

            <polygon points="200,230 280,270 200,310 120,270" class="svg-diamond"/>
            <text x="200" y="265" class="svg-text">数据库保存</text>
            <text x="200" y="280" class="svg-text">成功?</text>

            <rect x="100" y="340" width="200" height="50" class="svg-rect"/>
            <text x="200" y="360" class="svg-text">使 Redis 缓存失效 (Del)</text>
            <text x="200" y="375" class="svg-text" style="font-size:10px;">(redisotel 自动创建 Span)</text>

            <polygon points="200,410 280,450 200,490 120,450" class="svg-diamond"/>
            <text x="200" y="445" class="svg-text">Redis Del</text>
            <text x="200" y="460" class="svg-text">出错 (非 Nil)?</text>

            <rect x="100" y="520" width="200" height="40" class="svg-rect"/>
            <text x="200" y="540" class="svg-text">设置最终 Span 状态</text>
            <text x="200" y="555" class="svg-text" style="font-size:10px;">(OK 或 OK 带警告)</text>

            <rect x="100" y="590" width="200" height="40" class="svg-rect"/>
            <text x="200" y="610" class="svg-text">返回用户数据 & nil 错误</text>

            <rect x="310" y="250" width="150" height="40" class="svg-rect"/>
            <text x="385" y="270" class="svg-text">记录错误 & 设状态</text>
            <rect x="310" y="310" width="150" height="40" class="svg-rect"/>
            <text x="385" y="330" class="svg-text">返回错误给 Handler</text>

            <rect x="310" y="430" width="150" height="40" class="svg-rect"/>
            <text x="385" y="450" class="svg-text">记录缓存错误 (警告)</text>


            <!-- Arrows -->
            <line x1="200" y1="60" x2="200" y2="90" class="svg-arrow"/>
            <line x1="200" y1="130" x2="200" y2="160" class="svg-arrow"/>
            <line x1="200" y1="210" x2="200" y2="230" class="svg-arrow"/>

            <line x1="280" y1="270" x2="310" y2="270" class="svg-arrow"/> <text x="295" y="260" class="svg-label">否</text>
            <line x1="385" y1="290" x2="385" y2="310" class="svg-arrow"/>
            <!-- Error path ends by returning error -->


            <line x1="200" y1="310" x2="200" y2="340" class="svg-arrow"/> <text x="225" y="325" class="svg-label">是</text>
            <line x1="200" y1="390" x2="200" y2="410" class="svg-arrow"/>

            <line x1="280" y1="450" x2="310" y2="450" class="svg-arrow"/> <text x="295" y="440" class="svg-label">是 (错误)</text>
            <line x1="385" y1="470" x2="200" y2="520" class="svg-arrow"/> <text x="300" y="495" class="svg-label">记录后继续</text>


            <line x1="200" y1="490" x2="200" y2="520" class="svg-arrow"/> <text x="175" y="505" class="svg-label">否 (成功/Nil)</text>
            <line x1="200" y1="570" x2="200" y2="590" class="svg-arrow"/>
            <line x1="200" y1="630" x2="200" y2="650" class="svg-arrow"/> <!-- Assuming end after return -->


        </svg>
    </div>
    <h3>解释:</h3>
    <ol>
        <li><strong>创建 Service Span:</strong> 同 <code>findUserByID</code>。</li>
        <li><strong>保存到数据库 (GORM Create):</strong> 调用 <code>db.WithContext(ctx).Create(user)</code> 将用户数据插入数据库。<code>otelgorm</code> 自动追踪此操作。如果发生错误，记录错误，设置 Span 状态为 Error，并立即将错误返回给上层 Handler，流程终止。</li>
        <li><strong>使缓存失效 (Redis Del):</strong> 如果数据库保存成功，则继续执行缓存失效。构造与 <code>findUserByID</code> 中相同的缓存 Key，并调用 <code>rdb.Del(ctx, cacheKey)</code>。<code>redisotel</code> 自动追踪此操作。</li>
        <li><strong>缓存失效错误处理:</strong>
            <ul>
                <li>检查 <code>rdb.Del</code> 返回的错误。如果错误不是 <code>redis.Nil</code>（<code>redis.Nil</code> 表示 Key 本来就不存在，不是错误），则认为缓存失效操作失败。</li>
                <li>记录错误到 Span，添加警告事件。</li>
                <li><span class="highlight">关键：缓存失效失败通常被视为非致命错误。</span>即使无法删除缓存，主要目标（用户已保存到数据库）已经达成。因此，记录错误后，流程继续。</li>
                <li>如果删除成功或 Key 本不存在 (<code>err == nil</code> 或 <code>errors.Is(err, redis.Nil)</code>)，则添加相应的成功事件。</li>
            </ul>
        </li>
        <li><strong>设置最终 Span 状态:</strong> 根据数据库操作是否成功以及缓存操作是否出错（非致命），设置最终的 Span 状态。如果数据库出错，状态已设为 Error。如果数据库 OK 但缓存出错，状态设为 OK 但消息体现警告。如果两者都 OK，状态设为 OK。</li>
        <li><strong>返回结果:</strong> <span class="highlight">即使缓存失效失败</span>，只要数据库保存成功，就向上层 Handler 返回创建成功的用户对象和 <code>nil</code> 错误。</li>
    </ol>
</section>

</body>
</html>